Skip to content

Rust 中的拷贝和克隆

克隆(Clone)

克隆是一种深拷贝(deep copy)机制,可以用于任何类型,包括堆上分配的数据:

  1. 定义特点:通过实现 Clone 特征并调用 clone() 方法,可能会复制堆上的数据。
  2. 适用类型:
    • 所有类型都可以实现 Clone 特征
    • 例如,StringVec<T>Box<T>
  3. 行为特点:
    • Rust 永远也不会自动创建数据的 深拷贝。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
    • 如果需要深度复制数据,可以实现 Clone 特征,同时必须实现 clone 方法
    • 会复制所有数据,包括堆上分配的数据,性能开销可能较大,特别是对于大型数据结构
rust
let s1 = String::from("hello");  
let s2 = s1.clone();  // 显式克隆,s1和s2都有效  
println!("s1 = {}, s2 = {}", s1, s2);  // 正常工作

这段代码能够正常运行,说明 s2 确实完整的复制了 s1 的数据。

如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 clone 来简化编程。但是对于执行较为频繁的代码(热点路径),使用 clone 会极大的降低程序性能,需要小心使用!

拷贝(Copy)

拷贝是一种浅拷贝(shallow copy)机制,只适用于存储在栈上的简单数据类型:

  1. 特点:通过实现 Copy 特征来启用,复制只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。
  2. 适用类型:
    • 所有整数类型,如 u32、i32
    • 布尔类型 bool
    • 浮点数类型,如 f64
    • 字符类型 char
    • 元组,当且仅当其包含的类型也都实现了 Copy 特征,比如,(i32, i32)Copy 的,但 (i32, String) 就不是
    • 不可变引用&T(但可变引用 &mut T 不可 Copy)
  3. 行为特点:
    • 自动发生,不需要显式调用任何方法(例如 clone)
    • 实现 Copy 必须先实现 Clone
    • 复制时不会发生所有权转移,复制后,原值和新值都可以使用,修改一个不会影响另一个
rust
fn main() {
    let x = 5;
    let y = x; // x被复制给y,x仍然可用,这里没有发生所有权的转移
    println!("x: {}, y: {}", x, y); // 输出:x: 5, y: 5
}

这段代码没有调用 clone,不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。原因是像整型这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效(xy 都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。

复制值的特征 Copy 和 Clone

在 Rust 中,CopyClone 是两个用于对象复制的 trait。它们之间有一些重要的区别。 Clone 特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。

派生 Clone 实现了 clone 方法,当为整个的类型实现 Clone 时,在该类型的每一部分上都会调用 clone 方法。这意味着类型中所有字段或值也必须实现了 Clone,这样才能够派生 Clone

例如,当在一个切片(slice)上调用 to_vec 方法时, Clone 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 to_vec 中返回的 Vector 需要拥有实例数据,因此, to_vec 需要在每个元素上调用 clone 来逐个复制。因此,存储在切片中的类型必须实现 Clone

一个类型要实现 Copy 必须先实现 Clone ,因为 CopyClone 的子特征。当性能是关键因素时,应该优先考虑 Copy 而不是 Clone,因为深拷贝会导致性能下降。 Copy 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),

当一个类型的内部字段全部实现了 Copy 时,你就可以在该类型上派上 Copy 特征。 一个类型如果要实现 Copy 它必须先实现 Clone 特征,因为一个类型实现 Clone 后,就等于顺便实现了 Copy

rust
#[derive(Copy, Clone)]  
struct Point {  
    x: i32,  
    y: i32,  
}

总之, Copy 拥有更好的性能,当浅拷贝足够的时候,就不要使用 Clone ,不然会导致你的代码运行更慢,对于性能优化来说,一个很大的方面就是减少热点路径深拷贝的发生。

其他

所有权转移其实可以分为两类:栈上数据的复制和堆上数据的转移,这也是非常符合直觉的,例如i32这种类型实现了Copy特征,可以存储在栈上,因此它就是复制行为,而String类型是引用存储在栈上,底层数据存储在堆上,因此转移所有权时只需要复制一下引用即可。(Rust中基本类型的赋值操作表现出 copy 的行为,复合类型的赋值操作表现出 move 的行为,如果想让复合类型的赋值操作表现出 copy 的行为,就要显式的调用 clone)

  • 对于栈上数据(如数组),move 操作可能导致深拷贝,导致性能下降,例如下面的LargeArray:
rust
struct LargeArray {
    a: [i128; 10000],
}

结构体是一个复合类型,它内部字段的数据存在哪里,就大致决定了它存在哪里。而该结构体里面的a字段是一个数组,而不是动态数组 Vec,数组是存储在栈上的数据结构。所以 LargeArray 是存于栈上的数据结构。这种类型发生移动时,会复制所有数据,此时性能较低,可以用 Box 让其存储在堆上,从而避免复制所有数据。

  • 对于堆上数据(如Box<T>或String),move 操作只复制引用
rust
fn main() {
    // 在栈上创建一个长度为1000的数组
    let arr = [0;1000];
    // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是自动调用 Copy 直接重新深拷贝了一份数据
    let arr1 = arr;

    // arr 和 arr1 都拥有各自的栈上数组,因此不会报错
    println!("{:?}", arr.len());
    println!("{:?}", arr1.len());

    // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
    let arr = Box::new([0;1000]);
    // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
    // 所有权顺利转移给 arr1,arr 不再拥有所有权
    let arr1 = arr;
    println!("{:?}", arr1.len());
    // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
    // println!("{:?}", arr.len());
}